/*
 * AdminClient.java
 *
 * Created on July 18, 2004, 3:05 AM
 * 
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.fpdev.apps.admin;

import org.fpdev.apps.admin.analysis.AnalysisOps;
import org.fpdev.util.gui.GUIFactory;
import org.fpdev.core.FPEngine;
import org.fpdev.core.basenet.*;
import org.fpdev.util.*;
import java.io.*;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import javax.swing.JOptionPane;
import org.fpdev.apps.Application;
import org.fpdev.core.Scenario;
import org.fpdev.core.SysProps;
import org.fpdev.core.transit.Route;
import org.fpdev.core.transit.SubRoute;
import org.fpdev.core.transit.Timetable;
import org.fpdev.apps.admin.gui.ACGUI;
import org.fpdev.apps.admin.gui.MapModeToolbar;
import org.fpdev.apps.admin.gui.dialogs.StartupDialog;
import org.fpdev.apps.admin.gui.dialogs.UnsavedChangesDialog;
import org.fpdev.core.data.QueryableDataConnection;
import org.fpdev.core.transit.CalendarService;

/**
 * The central class for the standalone GUI-based administration client
 * application.
 * 
 * Major components include the engine (shared with the server), the 
 * GUI, and various "Ops" classes that handle specific user operations.
 * 
 * @author  demory
 */

public class AdminClient extends Application {

  private ACGUI gui_;

  private Scenario activeScen_;

  // the Ops classes:
  private RouteOps routeOps_;
  private StationOps stationOps_;
  private AnalysisOps analysisOps_;
  private LocationOps locationOps_;
  private NetworkOps networkOps_;
  private DatabaseOps dbOps_;
  private DataPackageOps dataPkgOps_;

  private ActionHistory history_;

  private SpecialNodeClickHandler specialNodeClickHandler_;
  private SpecialLinkClickHandler specialLinkClickHandler_;

  private RunGC runGC_;


  public AdminClient() {
    super("viewer", true);
    
    specialNodeClickHandler_ = null;
    specialLinkClickHandler_ = null;

    Runtime runtime = Runtime.getRuntime();
    System.out.println("mem startup: " + (runtime.totalMemory() - runtime.freeMemory()));

    activeScen_ = engine_.getScenarios().getBase();

    routeOps_ = new RouteOps(engine_.getRoutes(), this);
    stationOps_ = new StationOps(this);
    analysisOps_ = new AnalysisOps(this);
    locationOps_ = new LocationOps(this);
    networkOps_ = new NetworkOps(this);
    dbOps_ = new DatabaseOps(this);
    dataPkgOps_ = new DataPackageOps(this);
    history_ = new ActionHistory(this);

    loadData();

    networkOps_.setMaxIDs(engine_.getBaseNet().getMaxNodeID(), engine_.getBaseNet().getMaxLinkID());

    gui_ = new ACGUI(this);
    engine_.setGUIOutputAcceptor(gui_);
    gui_.setVisible(true);

    gui_.getStationSelectionPanel().updateStations();
    gui_.getRouteSelectionPanel().updateProviders();
    gui_.getRouteSelectionPanel().updateRoutes();

    gui_.onNewMode(gui_.getMode());
    gui_.getMapPanel().startedUp();
    System.gc();

    guiInitialized();
  }

  public void loadData() {

    String home = engine_.getProperty("5pHome"), fs = File.separator;
    
    if(engine_.getDataPackage().getProperty(SysProps.INIT_X) == null || engine_.getDataPackage().getProperty(SysProps.INIT_Y) == null) {
      System.out.println("ERROR: no initial location selected");
      System.exit(1);
      // todo: load avg from db nodes table instead of exiting
    }
   
    //boolean loadNetworkCache = engine_.getDataPackage().getProperty("loadNetworkCache").equals("true");

    /*if (loadNetworkCache) {
      engine_.readNetworkCacheFile();
    } else {*/

    String dataSrc = engine_.getDataPackage().getProperty("dataSource");
    boolean preloadNetwork = engine_.getDataPackage().getProperty("preloadNetwork").equals("true");

    /*if(dataSrc.equals("shp")) {
      engine_.getDataPackage().readNetworkFromSHP();
    }

    if(dataSrc.equals("postgis")) {
      engine_.getDataPackage().readNetworkFromPostgis();
    }*/

    if(preloadNetwork) {
      if(dataSrc.equals("shp") || dataSrc.equals("postgis")) {
        engine_.getDataPackage().readEntireNetwork();
      }
    }
    else {
      ((QueryableDataConnection) engine_.getDataPackage().getSpatialDataConn()).buildFIDLookup();
    }

    if(dataSrc.equals("derby") || dataSrc.equals("mysql")) { // legacy
      engine_.loadNodesFromDB(true);
      engine_.loadLinksFromDB(true);

      engine_.loadAddrRangesFromDB();
      engine_.loadShapePointsFromDB();
      engine_.loadElevInfoFromDB();
      engine_.loadScenLinkTypesFromDB();
    }

    // phase out..
    System.out.println("init link bbxs...");
    for(BLink link : engine_.getBaseNet().getLinks()) link.updateBBXY();
    System.out.println("done");

    
    System.out.println("created " + engine_.getBaseNet().linkCount() + " links");


    String dpName = engine_.getDataPackage().getName();

    //if(preloadNetwork) {

    boolean loadTransitCache = engine_.getDataPackage().getProperty("loadTransitCache").equals("true");
    if (loadTransitCache) {
      engine_.getRoutes().readFromCacheFile(home + "data" + fs + dpName + fs + "routes.fpc", gui_);
    } else {
      engine_.getRoutes().readFromXMLFile();
      engine_.getStations().readFromXML(home + "data" + fs + dpName + fs + "stations.xml");
    }

    if(preloadNetwork) {
      engine_.getRoutes().initPaths();
      engine_.getStations().initNetworkElements();
    }


      //engine_.getStations().readFromXML(home + "data" + fs + dpName + fs + "stations.xml");
    //}

    // not worrying about geocoding for now..
    //engine_.readStreetAliasFile(home + "data" + fs + dpName + fs + "streetalias.xml");
    //engine_.readZipsFile(home + "data" + fs + dpName + fs + "zips.xml");

    //System.out.println("mem after init: " + (runtime.totalMemory() - runtime.freeMemory()));
    System.gc();
    //System.out.println("mem after gc: " + (runtime.totalMemory() - runtime.freeMemory()));

    // old gui


  }

  public void guiInitialized() {
    // check with user about bad ids for elevation/shapepts/etc

    Set<Integer> ids = engine_.getBadAddrLinkIDs();
    if (!ids.isEmpty()) {
      int result = JOptionPane.showConfirmDialog(gui_, ids.size() + " invalid address range entries found. Delete?");
      if (result == JOptionPane.YES_OPTION) {
        Iterator<Integer> idIter = ids.iterator();
        while (idIter.hasNext()) {
          engine_.getDB().deleteLink(idIter.next());
        }
      }
    }

    ids = engine_.getBadShpPtsLinkIDs();
    if (!ids.isEmpty()) {
      int result = JOptionPane.showConfirmDialog(gui_, ids.size() + " invalid shapepoint entries found. Delete?");
      if (result == JOptionPane.YES_OPTION) {
        Iterator<Integer> idIter = ids.iterator();
        while (idIter.hasNext()) {
          engine_.getDB().deleteLink(idIter.next());
        }
      }
    }

    ids = engine_.getBadElevLinkIDs();
    if (!ids.isEmpty()) {
      int result = JOptionPane.showConfirmDialog(gui_, ids.size() + " invalid elevation entries found. Delete?");
      if (result == JOptionPane.YES_OPTION) {
        Iterator<Integer> idIter = ids.iterator();
        while (idIter.hasNext()) {
          engine_.getDB().deleteLink(idIter.next());
        }
      }
    }

    if(engine_.getProperty(SysProps.INIT_SHP) != null) {
      networkOps_.importStreetsWizard(engine_.getProperty(SysProps.INIT_SHP));
    }

  }

  /**
   * Method called by Application class prior to initializing the engine.
   * Assuming no startup datapkg is specified, the datapkg manager dialog is
   * displayed, and the user-selected/created package is then passed to the
   * engine (via the sysProps object).
   * 
   * @param sysProps system properties as loaded by the application class
   */
  @Override
  public void runPreEngineOps(Properties sysProps) {
    System.out.println("viewer pre-engine ops");
    
    String initDP = sysProps.getProperty(SysProps.INIT_DP);
    if(initDP == null || initDP.length() == 0) {
      // show dialog
      StartupDialog startup = new StartupDialog(sysProps);
      if(startup.getInitDPName() == null) {
        System.out.println("Startup dialog canceled, exiting..");
        System.exit(0);
      }
      sysProps.setProperty(SysProps.INIT_DP, startup.getInitDPName());
    }
  }
  
  public RouteOps getRouteOps() {
    return routeOps_;
  }

  public StationOps getStationOps() {
    return stationOps_;
  }

  public AnalysisOps getAnalysisOps() {
    return analysisOps_;
  }

  public NetworkOps getNetworkOps() {
    return networkOps_;
  }

  public DatabaseOps getDatabaseOps() {
    return dbOps_;
  }

  public DataPackageOps getDataPackageOps() {
    return dataPkgOps_;
  }

  public ActionHistory getHistory() {
    return history_;
  }

  public ACGUI getGUI() {
    return gui_;
  }

  public Scenario getActiveScenario() {
    return activeScen_;
    //return gui_.getScenariosPanel().getActiveScenario();
  }

  public void setActiveScenario(Scenario scen) {
    activeScen_ = scen;
  }

  public void expectSpecialNodeClick(SpecialNodeClickHandler snch) {
    specialNodeClickHandler_ = snch;
    gui_.setSpecialClickAction(ACGUI.SPECIAL_SELNODE);
  }

  public void clearSpecialNodeClickHandler() {
    specialNodeClickHandler_ = null;
  }

  @SuppressWarnings("static-access")
  public void expectSpecialLinkClick(SpecialLinkClickHandler slch) {
    specialLinkClickHandler_ = slch;
    gui_.setSpecialClickAction(gui_.SPECIAL_SELLINK);
  }

  public void clearSpecialLinkClickHandler() {
    specialLinkClickHandler_ = null;
  }

  public void msg(String text) {
    gui_.msg(text);
  }

  public void warning(String text) {
    msg("Warning: " + text);
  }

  public void setProgress(int value) {
    gui_.setProgress(value);
  }

  public void cancelCurrentOperation() {
    System.out.println("mca: " + gui_.getSelectedToolbarAction());
    switch (gui_.getSelectedToolbarAction()) {
      case MapModeToolbar.ACT_RDIVERTPATH:
        routeOps_.cancelDiversionPath();
        break;
    }
  }

  ////////////////////////  ANALYSIS  ///////////////////////

  public void initTransitElements() {
    engine_.initTransitAnnotations();
    engine_.buildRouteNetwork();
  }

  public void clearTransitElements() {
    engine_.clearRouteNetwork();
    engine_.clearTransitAnnotations();
  }

  public void specifyLocation(String locStr) {
    List<Location> locs = engine_.getLocations().resolveLocationFromInput(locStr);
    if (locs.size() == 0) {
      msg("Could not resolve location");
      return;
    }
    Location loc = null;
    if (locs.size() > 1) {
      //msg("resolved multiple");
      //return;
      List<String> strList = new LinkedList<String>();
      Iterator<Location> locsIter = locs.iterator();
      while (locsIter.hasNext()) {
        strList.add(locsIter.next().getDispText(false));
      }
      int i = GUIFactory.showSelectItemDialog(gui_, strList.iterator());
      if (i == -1) {
        return;
      }
      loc = locs.get(i);
    } else {
      loc = locs.get(0);
    }
    gui_.getMapPanel().recenter(loc.getNode().getX(), loc.getNode().getY());

  }

  public void switchDataPackage() {
    Properties sysProps = engine_.getProperties();
    StartupDialog startup = new StartupDialog(sysProps);

    if(startup.getInitDPName() == null) return;

    sysProps.setProperty(SysProps.INIT_DP, startup.getInitDPName());
    reinitializeEngine(sysProps);
    
    routeOps_.setRoutes(engine_.getRoutes());
    stationOps_.setStations(engine_.getStations());
    gui_.getMapPanel().getCanvas().setBaseNet(engine_.getBaseNet());

    loadData();


    // update gui components
    gui_.getRouteSelectionPanel().updateProviders();
    gui_.getRouteSelectionPanel().updateRoutes();
    gui_.getRouteSelectionPanel().updateSubs();
    gui_.getStationSelectionPanel().updateStations();
    gui_.getScenariosPanel().updateScenarios();

    gui_.getMapPanel().resetView(new Double(engine_.getDataPackage().getProperty(SysProps.INIT_X)),
                                 new Double(engine_.getDataPackage().getProperty(SysProps.INIT_Y)),
                                 new Double(engine_.getDataPackage().getProperty(SysProps.INIT_RES)));

    gui_.getMapPanel().refresh(true, true, true);

    guiInitialized();
  }

  public void shutDown() {
    
    // check for unsaved changes

    List<UnsavedChange> changes = new LinkedList<UnsavedChange>();

    for(Route rte : engine_.getRoutes().getCollection()) {
      for(SubRoute sub : rte.getSubRoutes()) {
        for(Timetable table : sub.getTables()) {
          if(!table.isFileSaved()) {
            changes.add(new TimetableChange(sub, table.getService()));
          }
        }
      }
    }
    if(!engine_.getRoutes().isXMLFileSaved())
      changes.add(new FileChange(FileType.ROUTES));
    if(!engine_.getStations().isXMLFileSaved())
      changes.add(new FileChange(FileType.STATIONS));


    if(!changes.isEmpty()) {
      UnsavedChangesDialog dialog = new UnsavedChangesDialog(gui_, changes);

      switch(dialog.getSelection()) {
        case CANCEL:
          return;
        case SAVE_AND_CLOSE:
          for(UnsavedChange change : changes) change.saveChange();
        case CLOSE_WITHOUT_SAVING:
      }
    }

    engine_.shutDown();
    System.exit(0);
  }

  public static interface UnsavedChange {
    public void saveChange();
  }

  private class TimetableChange implements UnsavedChange {
    private SubRoute sub_;
    private CalendarService service_;

    public TimetableChange(SubRoute sub, CalendarService service) {
      sub_ = sub;
      service_ = service;
    }

    public void saveChange() {
      sub_.writeTableToFile(service_, engine_.getDataPackage().getPath());
    }

    @Override
    public String toString() {
      return "Timetable for "+sub_.getMasterID()+" ("+service_.getName()+")";
    }
  }

  private enum FileType { ROUTES, STATIONS };

  private class FileChange implements UnsavedChange {

    private FileType type_;

    public FileChange(FileType type) {
      type_ = type;
    }

    public void saveChange() {
      switch(type_) {
        case ROUTES: routeOps_.writeRoutesFile();
        case STATIONS: stationOps_.writeStationsFile();
      }
    }

    @Override
    public String toString() {
      switch(type_) {
        case ROUTES: return "Master routes file";
        case STATIONS: return "Master stations file";
      }
      return "err";
    }
  }

  // EVENT-HANDLER

  public void fireEvent(ACEvent e) {
    System.out.println("event "+e.getType());

    if(AnalysisOps.isAnalysisEvent(e)) analysisOps_.handleAnalysisEvent(e);
    if(RouteOps.isRouteEvent(e)) routeOps_.handleRouteEvent(e);
    if(NetworkOps.isNetworkEvent(e)) networkOps_.handleNetworkEvent(e);
    if(DatabaseOps.isDatabaseEvent(e)) dbOps_.handleDatabaseEvent(e);

    switch (e.getType()) {

      // General Map Click Events
      case EventTypes.MCLICK_RECENTER:
        gui_.getMapPanel().recenter(e.getDoubleProperty("worldX"), e.getDoubleProperty("worldY"));
        break;
        
      case EventTypes.SPECIAL_CLICK_NODE:
        if (specialNodeClickHandler_ != null) {
          specialNodeClickHandler_.handleNode(e.getNodeProperty("node"));
        }
        break;

      case EventTypes.SPECIAL_CLICK_LINK:
        if (specialLinkClickHandler_ != null) {
          specialLinkClickHandler_.handleLink(e.getLinkProperty("link"));
        }
        break;

      // File Menu Events

      case EventTypes.FILEMENU_SWITCH_DATA_PACKAGE:
        switchDataPackage();
        break;

      case EventTypes.FILEMENU_WRITE_NETWORK_SHP:
        engine_.getDataPackage().writeNetworkToSHP(gui_);
        break;

      case EventTypes.FILEMENU_WRITE_NETWORK_POSTGIS:
        engine_.getDataPackage().writeNetworkToPostgis(gui_);
        break;

      case EventTypes.FILEMENU_WRITE_NETWORK_CACHE:
        engine_.writeNetworkCacheFile();
        break;

      case EventTypes.FILEMENU_READ_NETWORK_CACHE:
        engine_.readNetworkCacheFile();
        break;

      case EventTypes.FILEMENU_WRITE_TRANSIT_CACHE:
        engine_.writeTransitCacheFile();
        break;

      case EventTypes.FILEMENU_READ_TRANSIT_CACHE:
        engine_.readTransitCacheFile();
        break;

      case EventTypes.FILEMENU_EXPORT_LINKS_SHAPEFILE:
        networkOps_.getShapefileIO().writeLinksShapefile();
        break;

      case EventTypes.FILEMENU_EXPORT_ROUTES_SHAPEFILE:
        networkOps_.getShapefileIO().writeRoutesShapefile();
        break;

      case EventTypes.FILEMENU_EXPORT_STATIONS_SHAPEFILE:
        networkOps_.getShapefileIO().writeStationsShapefile();
        break;

      case EventTypes.FILEMENU_EXIT:
        shutDown();
        break;

      // Edit Menu Events

      case EventTypes.EDITMENU_UNDO:
        gui_.getMapPanel().cancelBuffer();
        cancelCurrentOperation();
        history_.undoLast();
        gui_.getMapPanel().refresh(true, true, true);
        break;

      case EventTypes.EDITMENU_REDO:
        gui_.getMapPanel().cancelBuffer();
        cancelCurrentOperation();
        history_.redoLast();
        gui_.getMapPanel().refresh(true, true, true);
        break;

      case EventTypes.EDITMENU_CANCEL_CURRENT_OPERATION:
        cancelCurrentOperation();
        break;

      // Tools menu

      case EventTypes.VIEWMENU_MANAGE_LANDMARKS:
        gui_.getLandmarksFrame().show();
        break;
      
      case EventTypes.VIEWMENU_MANAGE_STREETALIASES:
        gui_.getStreetAliasFrame().show();
        break;
        
      
      case EventTypes.MAPMENU_REVERSE_SHPPTS:
        Iterator<BLink> links = gui_.getMapPanel().getSelectedLinks().iterator();
        while (links.hasNext()) {
          BLink link = links.next();
          Collections.reverse(link.getShapePointsList());
          engine_.getDB().updateShapePoints(link);
        }
        gui_.getMapPanel().getSelectedLinks().clear();
        gui_.getMapPanel().refresh(false, true, true);
        break;

      // Help & Debug menus
        
      case EventTypes.HELPMENU_ABOUT:
        JOptionPane.showMessageDialog(gui_, "Five Points Viewer, version " + FPEngine.VERSION);
        break;

      case EventTypes.DEBUGMENU_GARBAGE_COLLECT:
        new RunGC(0);
        break;
      
      case EventTypes.DEBUGMENU_ACTION:
        debugAction();
        break;
        
      // Landmark Events

      case EventTypes.LOC_NEW_LANDMARK:
        locationOps_.newLandmark();
        break;

      case EventTypes.LOC_INIT_LANDMARK:
        locationOps_.initLandmark(e.getStringProperty("name"), e.getStringProperty("loc"), e.getStringProperty("altnames"));
        break;

      case EventTypes.LOC_UPDATE_LANDMARK:
        locationOps_.updateLandmark(e.getStringProperty("name"), e.getStringProperty("loc"), e.getStringProperty("altnames"));
        break;

      /*case EventTypes.LOC_NEW_LANDMARK:
      locationOps_.newLandmark(); break;*/
    }

  }

  ///// DEBUG
  public void debugAction() {
    //new GTFSImporter(this);

    System.out.println(""+Calendar.WEDNESDAY);
  }

  /**
   * @param args the command line arguments
   */
  public static void main(String args[]) {
    new AdminClient();
  }
}






























